A heatmap based on paddling workouts recorded on my Garmin watch
Over the past two years, I’ve logged dozens of paddling workouts with my Garmin watch. I wanted to learn more about my paddling habits over the past year, so I downloaded my Garmin data in multiple formats to use for analyses below. Read till the end to learn how I made a heatmap showing all the areas I paddled in 2024!
As any data person will tell you, acquiring the data is usually the most tedious part - and it was. I first logged into Garmin Connect and use their advanced search function to generate a .csv of all my paddling workouts in 2024. This part was easy. But of course I can never do anything basic, so I had to get fancy and I wanted GPS data to do that.
In order to get the GPS data I wanted, I had to log into my Garmin account, filter my activities to water sports, and navigate to each individual recorded activity in order to download it as a GPX file. This was in order to preserve the location data associated with each activity, which is not available in the summary .csv I had originally downloaded.
| Metric | Value |
|---|---|
| Total Distance Paddled (miles) | 603.93 |
| Total Hours Paddled | 120.41 |
| Number of Sessions | 87 |
| Average Distance per Session (miles) | 6.94 |
| Average Duration per Session (mins) | 83.04 |
| Earliest Start Time of Day | 06:23:11 |
| Latest End Time of Day | 11:27:01 |
| Total Calories Burned | 46942 |
| Average Heart Rate (bpm) | 141.2 |
| Max Heart Rate (bpm) | 211 |
| Total Strokes | 271583 |
On a side note, I don’t believe the metric “Latest end time of day” is correct. If it’s 11pm, that’s wayyy too late to be on the water. If it’s 11am, that’s far too early to be off the water, because most of our practices are in the evenings. I’m sure something just got recorded wrong there.

Quite a normal distribution until we get to change season! And you can bet your ass I did absolutely zero paddling in October. A girl’s gotta rest.
Once I had an individual file for each activity (87 in total!), I had to aggregate it all into one file. I did this in a separate processing script, so that I could save one small, efficient file to this website and its associated GitHub repo. The code below will not run, it’s just to showcase how I did this processing.
library(sf)
library(xml2)
library(dplyr)
library(purrr)
library(lubridate)
library(jsonlite)
# Define the folder containing GPX files
gpx_folder <- "data/2024 Garmin Data/"
# Get a list of all GPX files in the folder
gpx_files <- list.files(gpx_folder, pattern = "\\.gpx$", full.names = TRUE)
print(gpx_files) # This should show a list of file paths
# Spot-check
gpx_sample <- read_xml(gpx_files[1]) # Read first file to make sure it worked
print(gpx_sample)
# Now use a function to parse all of the GPX files:
extract_gpx_distance <- function(file) {
gpx <- read_xml(file) %>% xml_ns_strip() # Strip namespace
coords <- gpx %>%
xml_find_all("//trkpt") %>%
map_df(~data.frame(
Latitude = as.numeric(xml_attr(.x, "lat")),
Longitude = as.numeric(xml_attr(.x, "lon")),
Timestamp = xml_text(xml_find_first(.x, "time")), # Extract timestamp
File = basename(file) # Keep track of source file
))
if (nrow(coords) < 2) {
return(NULL) # Skip files with too few points
}
# Convert Timestamp to proper datetime format
coords <- coords %>%
mutate(
Timestamp = ymd_hms(Timestamp), # Convert to POSIXct
Date = as.Date(Timestamp) # Extract Date separately
) %>%
arrange(Timestamp) # Ensure chronological order
# Compute distances between consecutive points
coords <- coords %>%
mutate(
Distance_m = c(0, distHaversine(cbind(Longitude[-n()], Latitude[-n()]),
cbind(Longitude[-1], Latitude[-1]))), # Compute distances
Cumulative_Distance_km = cumsum(Distance_m) / 1000 # Convert meters to km
)
return(coords)
}
# Run the function on all files
all_gpx_data <- map_df(gpx_files, extract_gpx_distance)
# Save data for future use
write.csv(all_gpx_data, "2024_paddling_routes.csv", row.names = FALSE)
# Convert to a spatial object
paddling_sf <- st_as_sf(all_gpx_data, coords = c("Longitude", "Latitude"), crs = 4326)
# Save as GeoJSON
st_write(paddling_sf, "paddling_data.geojson", driver = "GeoJSON", append = FALSE)
And voila! Now I have a GeoJSON file available to make fun maps with!